2019-12-30 this:呼叫函數的人 勘誤中,參考:討論文
昨天提到了用 debug 模式玩ES6的基本語法。
以 VSCode dubug 模式來看,經典的觀念:作用域(Scope)、this、閉包(Closure)
函數可看成一群程式碼的集合,可以幫我們包裝 routine 的工作 (可以重複呼叫),命名後可以增加程式碼可讀性。
函數也引發了變數作用域、閉包、this 的問題。
有兩種方法可以宣告函數
function sayHi() {
console.log('Hi!)');
}
const sayHi = () => {
console.log('Hi!');
}
var/let/const
作用域(Scope): 變數生存的空間接下來會用 debug 模式,觀察 var/let
的特性。
在 ES6 出來以前只有 var
可以用,這是指在宣告在函數內的變數,在這函數的執行過程中會一直在,不管包幾層區塊。
{…}
) 內宣告的變數可以在區塊外使用嗎?我們觀察以下程式碼:
// var/let in global
const runIf = true;
if(runIf) {
var ifVar = 'ifVar'; // if 執行完會留下
let ifLet = 'ifLet';
}
console.log(ifVar); // 執行到這行,可存取到 ifVar,因為 ifVar 是在主程式函數中宣告的
// console.log(ifLet); // ReferenceError: ifLet is not defined
// var/let in function
function fun1() {
var innerVar = 'fun1Let'; // fun1()執行完不會留下
let innerLet = 'fun1Let'; // fun1()執行完不會留下
}
fun1();
// console.log(innerVar); // ReferenceError: innerVar is not defined
// console.log(innerLet); // ReferenceError: innerLet is not defined
console.log('bye');
在第2,5,13,19行下中斷點,執行 debug,如下圖:
停在第2行後,看看 CALL STACK ,目前執行在匿名函數中 (anonymous function)中,也就是,當程式執行時,我們可以假想它們被包在某個函數中且立刻被執行,像是:
(anonymousFunction() {
// …上面程式碼…
})()
此外, VARIABLES->Local 中有在函數內可以存取的變數,但會發現 沒有 ifLet
,也就是第8行不能讀到ifLet
的原因。
再往下執行到第5行,
多出 VARIABLES->Block ,裡面有ifLet
,而 VARIABLES->Local 還留著。
再往下執行到第13行,
CALL STACK 現在進入到 fun1
中, CALL STACK 自然就只剩下 innerLet
和 innerVar
( this 晚點說)
再往下執行到第19行,
離開 fun1()
後 innerLet
和 innerVar
就會被消毀,當再次回到「進來前的函數空間」, innerLet
和 innerVar
當然就存取不到了,也就是第17,18行不能讀到他們。
可以試試把第 8, 17, 18註解拿掉,會丟出例外
我們整理結論:
var
是屬於函數作用域(function scope),活在函數中,出現在 VARIABLES->Local 中let
是屬於區塊作用域(block scope),活在 {…}
(curly brackets),出現在 VARIABLES->Block 中那…const
呢? 它跟 let
一樣,只是變數不能再次被賦值(=
)。
所以結論:
var
是屬於函數作用域(function scope),活在函數中,出現在 VARIABLES->Local 中let/const
是屬於區塊作用域(block scope),活在 {…}
(curly brackets),出現在 VARIABLES->Block 中我的做法是變儘量限制它們的 scope,以降低無法預期的效果,像是:var 變數生存太久佔用記憶體、本應該是常數的東西不小心執行時被改到、存取到本不應該存在的變數…等
const
儘量用let
var
this
在OO(物件導向)技術被用來當做實例(instance)的代理變數。在 javascript 也有類似的功用,但 this
是可以被我們動態替換的,所以可以做的更多,見:JavaScript - call,apply,bind。現階段只要了解:this就是呼叫函數的人
看以下的程式,下中斷點觀察
const funA = () => {
console.log(this);
};
function funF () {
console.log(this);
};
const obj = {
funA: funA,
funF: funF,
}
funA();
funF();
obj.funA();
obj.funF();
下面這張圖,程式是從第14行進入至第3行,呼就叫的人是誰?因為沒有指明人,就會拿最上層的人(物件),所以就叫 global
,此時 this
是 global
下面這張圖,程式是從第17行進入至第3行,呼就叫的人是誰?是 obj
,所以 this
是 obj
且它的型別是 Object
,打開來看看真的是它
我們考慮以下問題:
回答這些問題
const outer = 'outer';
function fun(a){
console.log(a);
}
fun(outer);
const outer = 'outer';
function fun(){
console.log(outer); // 引用到外部變數,所以會放到 fun()函數中的閉包域
}
fun();
執行以下程式,並下中斷點,為了看的更清楚我們很刻意的放到 main() 中執行,
function main() {
let outer = 'outer'; // 外部變數
function funA() {
console.log(outer); // 讀取到外部變數
};
function funB() {
const inner = outer; // 內部變數,指向 outer 的值
outer = outer + '-fix'; // 修改 outer 的值
console.log(inner, outer);
};
funA();
funB(); // outer 值被修改
funA();
};
main();
下圖中,outer
放入 VARIABLES->Closure 閉包域中,使我們可以存取它的值。
下圖中,因為宣告了 const inner
,它是屬於 VARIABLES->Local ,並設定成outer
的值,所以 inner = 'outer'
。然而,outer = outer + '-fix'
,把 outer
改成了 outer-fix
的值。此外,outer
也被放入 VARIABLES->Closure 閉包域中。
用記憶體圖示來說,就會很清楚了,白正方形是記憶體空間,裡面會放字串值。
最後在呼叫一次 funA()
,得到 outer
被修改後的結果。
可以用閉包包入定值,但很煩,ES6 引入的 let 可以簡化不少。
// i, j loop 完,變成定值
let funs = [];
for (var i = 0; i < 3; i++) {
var j = i;
funs.push(function () {
console.log(i, j); // loop 完才用到 i, j
});
}
funs.forEach(fun => fun());
// 用閉包
funs = [];
for (var i = 0; i < 3; i++) {
(function () {
var j = i; // 把值存入一個匿名函數的閉包
funs.push(function () {
console.log(i, j);
});
})();
}
funs.forEach(fun => fun());
// 用 let
funs = [];
for (var i = 0; i < 3; i++) {
let j = i;
funs.push(function () {
console.log(i, j);
});
}
funs.forEach(fun => fun());
結果:
3 2
3 2
3 2
3 0
3 1
3 2
3 0
3 1
3 2
只有後面兩種寫法會正確。
可以猜看看下面的結果:
const funs = [];
for (var i = 0; i < 3; i++) {
const j = i;
funs.push(function () {
const inner = i;
console.log(inner, i, j);
});
}
funs.forEach(fun => fun());
今天用 debug 模式,觀察作用域(Scope)、this、閉包(Closure)的例子,並發現下面的關連性。
VARIABLES->Local - var
VARIABLES->Block - let/const
VARIABLES->Closure - 閉包
funs.forEach(fun => fun());
fun => fun() 這是什麼意思 ?
fun => fun()
是一個箭頭函數,可以想成
function (fun){
return fun();
}
箭頭函數回傳若不同時寫 {}
和return
,就是直接回傳
funs
從命名來看是一個 ”函數”的陣列,所以就是歷遍陣列內的所有函數。
funs.forEach(
function (fun){
return fun();
}
);
其中下面這一段我也看不懂它的包法!!
(function () {
var j = i; // 把值存入一個匿名函數的閉包
funs.push(function () {
console.log(i, j);
});
})();
謝謝你的提問,這問題問的很好
上層的(function () {})
匿名函數,把 i
放入此匿名函數的閉包。假設 i = 0
,因為(function () {})()
會立刻執行,一執行到var j = i
時,會進行賦值 j
被設為 0,且這 j
是這匿名函數的Local空間。
然後看
funs.push(function () {
console.log(i, j);
});
這裡的
function () {
console.log(i, j);
}
又產生了閉包,j 被它包著了,而它是來自上一個匿名函數且值是 0。
當 loop 結束後,
function () {
console.log(i, j);
}
一個個被執行,j
是來自上一層匿名函數的值,每個上一層匿名函數有自己的 j
,且值都不一樣。
然而i
就不一樣了,他是來自全域的,loop 結束後值就是 3,所以才會得到
3 0
3 1
3 2
把中斷點下在
var j = i; // 把值存入一個匿名函數的閉包
console.log(i, j);
你可以比較清楚看到執行過程
你好,我在做 this
部分的實作時出現一個問題,在開始debugging從第13行執行到第2行時,VARIABLES顯示在funcA hapi 的this會是一個空的Object:
以下是我的launch.json設定
請問可能是什麼原因造成的呢?
跟你一樣的狀況
funA();
funF();
obj.funA();
的this都是global
只有obj.funF();的this才是object
我現在用 Nodejs 12 跟你們的的結果一樣,跟內文有差異。
箭頭函數和一般函數是不一樣的東西,在{}
裡面使用 this
是不同的意義。在箭頭函數中用this
時,其值會與宣告箭頭函數的 lexical context
相同。一般函數的 this
才會是呼叫函數的人。
我對於 this:呼叫函數的人 的解釋不完全正確,請見諒,我想需要說明並校正內容。
參考: